package org.xx.armory.rpc.routing;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.objectparser.LineInfo;
import org.xx.armory.objectparser.ParsedObject;
import org.xx.armory.rpc.RpcVerb;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.SysUtils.stringEquals;
import static org.xx.armory.commons.Validators.notBlank;

/**
 * 路由。
 * <p>
 * 通过{@link #match(String, RpcVerb)}方法来判断指定的URL是否可以匹配此路由, 并计算出路由数据。
 * </p>
 * <p>
 * 路由数据只有在匹配成功后才可以计算出。比如路由的样式是 {@literal /abc/ name} , 待匹配的URL是 {@literal /abc/def}, 那么匹配可以成功, 匹配数据是
 * {@code name=def}。
 * </p>
 *
 * @author Haart
 * @see RouteData
 * @see RouteTable
 */
public final class Route
        extends ParsedObject {
    private static final Pattern PLACE_HOLDER = Pattern.compile(
            "(?<![${}]) # Leading chars.\n"
                    + "\\$\\{([a-z0-9_-]+?)} # Placeholder name.\n"
                    + "(?![${}]) # Tailing chars.\n",
            Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.COMMENTS);
    private final Logger logger = LoggerFactory.getLogger(Route.class);
    private final String name;
    private final String pattern;
    private final String className;
    private final String methodName;
    private final RpcVerb verb;
    private final boolean enabled;
    private final String description;
    private final Collection<RouteToken> tokens;
    private final int[] permissions;

    /**
     * 构造路由实例。
     *
     * @param lineInfo
     *         源文件中的位置信息。
     * @param name
     *         路由名称, 会被正规化。
     * @param pattern
     *         路由的URL样式, 会被正规化。
     * @param className
     *         关联的类名, 会被正规化。
     * @param methodName
     *         关联的方法名, 会被正规化。
     * @param verb
     *         rpc请求方法名。
     * @param permissions
     *         使用此路由需要的权限。
     * @throws IllegalArgumentException
     *         如果参数{@code pattern}是{@code null}或者参数{@code className}是{@code null}。如果参数{@code pattern}只包含空白字符, 或者参数{@code pattern}的开头不是斜线({@code "/"}), 或者参数
     *         {@code pattern}的结尾是斜线({@code "/"}), 或者参数{@code verb}的值无法处理。
     */
    public Route(
            LineInfo lineInfo,
            String name,
            String pattern,
            String className,
            String methodName,
            RpcVerb verb,
            int[] permissions
    ) {
        this(lineInfo, name, pattern, className, methodName, verb, true, "", permissions, null, null);
    }

    /**
     * 构造路由实例。
     *
     * @param lineInfo
     *         源文件中的位置信息。
     * @param name
     *         路由名称, 会被正规化。
     * @param pattern
     *         路由的URL样式, 会被正规化。
     * @param className
     *         关联的类名, 会被正规化。
     * @param methodName
     *         关联的方法名, 会被正规化。
     * @param verb
     *         rpc请求方法名。
     * @param enabled
     *         此路由是否可用。
     * @param description
     *         针对此路由的描述。
     * @param permissions
     *         使用此路由需要的权限。
     * @throws IllegalArgumentException
     *         如果参数{@code pattern}是{@code null}或者参数{@code className}是{@code null}。如果参数{@code pattern}只包含空白字符, 或者参数{@code pattern}的开头不是斜线({@code "/"}), 或者参数
     *         {@code pattern}的结尾是斜线({@code "/"}), 或者参数{@code verb}的值无法处理。
     */
    public Route(
            LineInfo lineInfo,
            String name,
            String pattern,
            String className,
            String methodName,
            RpcVerb verb,
            boolean enabled,
            String description,
            int[] permissions
    ) {
        this(lineInfo, name, pattern, className, methodName, verb, enabled, description, permissions, null, null);
    }

    /**
     * 构造路由实例。
     *
     * @param lineInfo
     *         源文件中的位置信息。
     * @param name
     *         路由名称, 会被正规化。
     * @param pattern
     *         路由的URL样式, 会被正规化。
     * @param className
     *         关联的模块的类名, 会被正规化。
     * @param methodName
     *         关联的模块的方法名, 会被正规化。
     * @param verb
     *         rpc请求方法名。
     * @param enabled
     *         此路由是否可用。
     * @param description
     *         针对此路由的描述。
     * @param permissions
     *         使用此路由需要的权限。
     * @param default_
     *         路由数据的缺省值, 如果路由中的某些占位符无法获取则使用缺省值。
     * @param restriction_
     *         路由数据的限制。
     * @throws IllegalArgumentException
     *         如果参数{@code pattern}是{@code null}或者参数{@code className}是{@code null}。如果参数{@code pattern}只包含空白字符, 或者参数{@code pattern}的开头不是斜线({@code "/"}), 或者参数
     *         {@code pattern}的结尾是斜线({@code "/"}), 或者参数{@code verb}的值无法处理。
     */
    public Route(
            LineInfo lineInfo,
            String name,
            String pattern,
            String className,
            String methodName,
            RpcVerb verb,
            boolean enabled,
            String description,
            int[] permissions,
            RouteData default_,
            RouteData restriction_
    ) {
        super(lineInfo);

        this.name = trimToEmpty(name);
        this.pattern = notBlank(pattern, "pattern").trim();
        this.className = notBlank(className, "className").trim();
        this.verb = verb;
        this.enabled = enabled;
        this.description = trimToEmpty(description);
        this.tokens = new ArrayList<>();
        this.permissions = permissions == null ? new int[0] : permissions;

        if (!this.pattern.startsWith("/") || this.pattern.endsWith("/")) {
            throw new IllegalArgumentException("illegal pattern \"" + this.pattern + "\" at " + this.getLineInfo());
        }

        methodName = trimToEmpty(methodName);
        if (methodName.isEmpty()) {
            switch (this.verb) {
                case GET:
                    this.methodName = "doGet";
                    break;
                case POST:
                    this.methodName = "doPost";
                    break;
                case PUT:
                    this.methodName = "doPut";
                    break;
                case DELETE:
                    this.methodName = "doDelete";
                    break;
                default:
                    throw new IllegalArgumentException("invalid verb: " + verb);
            }
        } else {
            this.methodName = methodName;
        }

        default_ = default_ == null ? new RouteData(null) : default_;
        restriction_ = restriction_ == null ? new RouteData(null) : restriction_;

        final Matcher mo = PLACE_HOLDER.matcher(pattern);
        int pos = 0;

        while (mo.find()) {
            tokens.add(new RouteText(pattern.substring(pos, mo.start())));

            final String tokenName = mo.group(1);
            tokens.add(new RoutePlaceHolder(tokenName, default_.get(tokenName, null), restriction_.get(tokenName, null)));

            // Move position.
            pos = mo.end(0);
        }

        if (pos != pattern.length()) {
            tokens.add(new RouteText(pattern.substring(pos)));
        }
    }

    /**
     * 获取路由的名称。
     *
     * @return 路由的名称。
     */
    public String getName() {
        return this.name;
    }

    /**
     * 获取路由的样式。
     *
     * @return 路由的样式。
     */
    public String getPattern() {
        return this.pattern;
    }

    /**
     * 获取关联的类名。
     *
     * @return 关联的类名。
     */
    public String getClassName() {
        return this.className;
    }

    /**
     * 获取关联的方法名。
     *
     * @return 关联的方法名。
     */
    public String getMethodName() {
        return this.methodName;
    }

    /**
     * 获取HTTP方法名。
     *
     * @return HTTP方法名。
     */
    public RpcVerb getVerb() {
        return this.verb;
    }

    /**
     * 判断此路由是否可用。
     *
     * @return 此路由是否可用。
     */
    public boolean isEnabled() {
        return this.enabled;
    }

    /**
     * 获取针对此路由的描述。
     *
     * @return 针对此路由的描述。
     */
    public String getDescription() {
        return this.description;
    }

    /**
     * 获取使用此路由需要的权限。
     *
     * @return 使用此路由需要的权限，如果未指定所需权限，则返回空数组。
     */
    public final int[] getPermissions() {
        return this.permissions;
    }


    /**
     * 判断HTTP访问方法是否匹配。
     *
     * @param expected
     *         期望的方法。
     * @param actual
     *         实际的方法。
     * @return 如果匹配则返回 {@code true}，否则返回 {@code false}。
     */
    private boolean matchMethod(
            RpcVerb expected,
            RpcVerb actual
    ) {
        return expected == actual;
    }

    /**
     * 尝试匹配指定的URL。
     *
     * @param url
     *         指定的URL。
     * @param httpMethod
     *         HTTP访问方法。
     * @return 如果能够匹配指定的URL则返回匹配结果, 否则返回{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code url}是{@code null}或者只包含空白字符。
     */
    public RouteData match(
            String url,
            RpcVerb httpMethod
    ) {
        url = notBlank(url, "url").trim();

        if (!url.startsWith("/") || url.endsWith("/")) {
            throw new IllegalArgumentException("illegal url: " + url);
        }

        if (!matchMethod(getVerb(), httpMethod)) {
            return null;
        }

        int pos = 0, i = 0;
        final RouteData data = new RouteData(this);
        for (final RouteToken token : tokens) {
            if (token == null) {
                throw new IllegalArgumentException("tokens[" + i + "] is null");
            }
            ++i;

            if (pos == url.length()) {
                // 已到达指定URL的结尾，但是仍有路由标记需要匹配。
                switch (token.getTokenType()) {
                    case PLACEHOLDER:
                        final RoutePlaceHolder placeHolder = (RoutePlaceHolder) token;
                        if (placeHolder.getDefault() == null) {
                            // 如果未指定缺省值，那么格式错误。
                            logger.trace("mismatched: [{}] {} expected more chars", i, token);
                            return null;
                        } else {
                            // 如果指定了缺省值，则使用缺省值。
                            logger.trace("use default value: [{}] {} <=== {}", i, token, placeHolder.getDefault());
                            data.put(token.getName(), placeHolder.getDefault());
                        }
                        continue;
                    case TEXT:
                        final RouteText text = (RouteText) token;
                        if (stringEquals(text.getContent(), "/")) {
                            // 如果未匹配的文本是斜杠，那么忽略。
                            logger.trace("ignore tailing slash: [{}] {}", i, token);
                        } else {
                            // 否则格式错误。
                            logger.trace("mismatched: [{}] {} expected more chars", i, token);
                            return null;
                        }
                        continue;
                    default:
                        throw new IllegalStateException("unknown token: " + token);
                }
            }

            final String text = token.match(url, pos);
            if (text == null) {
                // 未匹配.
                logger.trace("mismatched: [{}] {} actual is {}", token, url.substring(pos));
                return null;
            } else {
                logger.trace("[{}] {} <=== {}", i, token, text);
                if (token.getTokenType() == RouteTokenType.PLACEHOLDER) {
                    data.put(token.getName(), text);
                }
            }

            pos += text.length();
        }

        if (pos != url.length()) {
            // 所有token都逐一匹配完成，但是待匹配文本还有剩余。
            logger.trace("mismatched: unexpected tailing chars {}", url.substring(pos));
            return null;
        }

        return data;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return Objects.hash(this.verb, this.pattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Route other = (Route) obj;
        return this.verb == other.verb && Objects.equals(this.pattern, other.pattern);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(String.format("\"%s\" [%6s] %s.%s", this.name, this.verb, this.className, this.methodName));

        int i = 0;
        for (final RouteToken token : tokens) {
            sb.append("\n");
            sb.append(String.format("[%-2d] %s", ++i, token));
        }
        return sb.toString();
    }
}
